Skip to main content

Struct Structures

Introduction

Apart from the most basic primitive types, only arrays are composite types, which can contain more than one value at a time, but can only contain data of the same type, which is not sufficient in practice.

In practice, there are two main situations where a more flexible and powerful composite type is needed, as follows

  • Complex objects need to be described using multiple variables that are all related, and it is desirable to have some mechanism to link them together.
  • Certain functions need to be passed multiple arguments, which are very cumbersome if passed one by one in order, and would be better combined and passed as a composite structure.

To solve these problems, C provides the struct keyword, which allows custom composite data types to combine values of different types together. C does not have the concept of objects and classes that other languages have, and the struct structure provides much of the functionality of objects and classes.

The following is an example of a struct custom data type.

struct fraction {
int numerator;
int denominator;
};

The above example defines a fraction datatype struct fraction, containing two attributes numerator and denominator.

Note that as a custom datatype, its type name should include the struct keyword, e.g. struct fraction in the above example. fraction alone does not make any sense, and the script could even define a separate variable named fraction, although this could easily cause confusion. Also, the semicolon at the end of the struct statement should not be omitted, as this could easily lead to errors.

Once you have defined a new data type, you can declare variables of that type, which is written in the same way as declaring variables of other types.

struct fraction f1;

f1.numerator = 22;
f1.denominator = 7;

In the above example, a variable f1 of type struct fraction is declared first, at which point the compiler allocates memory for f1, and you can then assign values to the different properties of f1. As you can see, the attributes of a struct structure are represented by dots (. ), e.g. the numerator property is written as f1.numerator.

As a reminder, when declaring variables of a custom type, do not forget to add the struct keyword in front of the type name. That is, the variable must be declared using struct fraction f1, not fraction f1.

In addition to assigning values to properties one by one, you can also use curly brackets to assign values to all properties of a struct at once.

struct car {
char* name;
float price;
int speed;
};

struct car saturn = {"Saturn SL/2", 16000.99, 175};

In the above example, the variable saturn is of type struct car, and the curly brackets assign values to all three of its properties at the same time. If the number of values inside the curly brackets is less than the number of properties, then the missing property is automatically initialized to 0.

Note that the order of the values inside the curly brackets must be the same as the order of the attributes at the time of the struct type declaration. Otherwise, the attribute name must be specified for each value.

struct car saturn = {.speed=172, .name="Saturn SL/2"};

In the above example, fewer properties are initialized than when they were declared, and those that remain are initialized to 0.

After declaring a variable, the value of a property can be modified.

struct car saturn = {.speed=172, .name="Saturn SL/2"};
saturn.speed = 168;

The above example changes the value of the speed attribute to 168.

The data type declaration statement of struct and the variable declaration statement can be combined into one statement.

struct book {
char title[500];
char author[100];
float value;
} b1;

The above statement declares both the data type book and the variable b1 of that type. If the type identifier book is only used in this one place and is not used again later, the type name can be omitted here.

struct {
char title[500];
char author[100];
float value;
} b1;

In the above example, struct declares an anonymous data type, and then declares the variable b1 of that type.

As with other variable declaration statements, it is possible to assign a value to a variable at the same time as declaring it.

struct {
char title[500];
char author[100];
float value;
} b1 = {"Harry Potter", "J. K. Rowling", 10.0},
b2 = {"Cancer Ward", "Aleksandr Solzhenitsyn", 7.85};

In the above example, the variables b1 and b2 are assigned values while they are declared.

The `typedef'' command, described in the next chapter, allows you to specify an alias for a struct structure, which makes it more concise to use.

typedef struct cell_phone {
int cell_no;
float minutes_of_charge;
} phone;

phone p = {5551234, 5};

In the above example, phone is an alias for struct cell_phone.

A pointer variable can also point to a struct structure.

struct book {
char title[500];
char author[100];
float value;
}* b1;

// Or write it as two statements
struct book {
char title[500];
char author[100];
float value;
};
struct book* b1;

In the example above, the variable b1 is a pointer to data that is an instance of type struct book.

A struct can also be used as a member of an array.

struct fraction numbers[1000];

numbers[0].numerator = 22;
numbers[0].denominator = 7;

The above example declares an array numbers with 1000 members, each of which is an instance of the custom type fraction.

The storage space occupied by the struct is not the sum of the individual attribute storage spaces. This is because for computational efficiency, the memory footprint of C must, in general, be a multiple of the storage of type int. If the storage of type int is 4 bytes, then the storage of type struct is always a multiple of 4.

struct { char a; int b; } s;
printf("%d\n", sizeof(s)); // 8

In the example above, the storage space for the variable s should be 5 bytes if you add up the space occupied by the attributes. However, the struct has a multiple of the int type, so the final result is 8 bytes, with a 3-byte "hole" between the a attribute and the b attribute.

Copying of structs

A struct variable can be copied to another variable using the assignment operator (=), which creates a completely new copy. A new block of memory, the same size as the original variable, is allocated and each attribute is copied over, i.e. a copy of the data is generated as is. This is not the same as copying arrays, so be careful.

struct cat { char name[30]; short age; } a, b;

strcpy(a.name, "Hula");
a.age = 3;

b = a;
b.name[0] = 'M';

printf("%s\n", a.name); // Hula
printf("%s\n", b.name); // Mula

In the above example, the variable b is a copy of the variable a, and the values of the two variables are independent of each other; modifying b.name does not affect a.name.

The above example assumes that the properties of the struct must be defined as an array of characters in order to copy the data. If the attribute is defined as a character pointer with a slight modification, the result will not be the same.

struct cat { char* name; short age; } a, b;

a.name = "Hula";
a.age = 3;

b = a;

In the above example, the name attribute becomes a character pointer, and the assignment of a to b results in b.name being the same character pointer to the same address, i.e. the two attributes share the same address. Because the struct is then holding a pointer, rather than the array from the previous example, it is not the string itself that is copied, but its pointer. Also, the string cannot be modified at this point, because the string pointed to by the character pointer cannot be modified.

To summarise, the assignment operator (=) makes an identical copy of the value of each attribute of a struct to another struct variable. This is completely different from arrays, where the assignment operator does not copy the data, only the address is shared.

Note that this type of assignment requires the two variables to be of the same type; struct variables of different types cannot be assigned to each other.

In addition, C does not provide a way to compare two custom data structures for equality, and it is not possible to use comparison operators (such as == and ! =) to compare two data structures for equality or inequality.

struct pointer

If a struct variable is passed into a function, what the function gets internally is a copy of the original value.

##include <stdio.h>

struct turtle {
char* name;
char* species;
int age;
};

void happy(struct turtle t) {
t.age = t.age + 1;
}

int main() {
struct turtle myTurtle = {"MyTurtle", "sea turtle", 99};
happy(myTurtle);
printf("Age is %i\n", myTurtle.age); // output 99
return 0;
}

In the above example, the function happy() is passed in a struct variable myTurtle and there is a self-increment operation inside the function. However, after the execution of happy(), the value of the age property outside the function does not change at all. The reason for this is that the function internally gets a copy of the struct variable, and changing the copy does not affect the original data outside the function.

Normally, the developer would like the same data to be passed into the function, so that any changes to the data inside the function are reflected outside the function. It is also beneficial to have the same data passed in to improve the performance of the program. This is where a pointer to a struct variable is passed into the function, and the struct attribute is modified by the pointer to affect the outside of the function.

A struct pointer passed into a function is written as follows.

void happy(struct turtle* t) {
}

happy(&myTurtle);

In the above code, t is a pointer to the struct, which is passed in when the function is called. struct types are not like arrays, the type identifier itself is not a pointer, so when passed in, the pointer must be written as &myTurtle.

The function must also be written internally with (*t).age to get the struct structure itself from the pointer.

void happy(struct turtle* t) {
(*t).age = (*t).age + 1;
}

In the above example, (*t).age cannot be written as *t.age, because the dot operator . has higher precedence than *. Writing *t.age in this way would treat t.age as a pointer and then take its corresponding value, with unpredictable results.

Now, recompile and execute the entire example above, and the operations on the struct inside happy() will be reflected outside the function.

Writing (*t).age in this way is cumbersome. C then introduces a new arrow operator (->) that gets the attribute directly from the struct pointer, greatly enhancing the readability of the code.

void happy(struct turtle* t) {
t->age = t->age + 1;
}

To summarize, for struct variable names, use the dot operator (. ) for struct variable names and the arrow operator (->) for struct variable pointers. Taking the variable myStruct as an example, and assuming that ptr is its pointer, the following three ways of writing it are the same thing.

// ptr == &myStruct
myStruct.prop == (*ptr).prop == ptr->prop

Nesting of structs

A member of a struct can be a member of another struct.

struct species {
char* name;
int kinds;
};

struct fish {
char* name;
int age;
struct species breed;
};

In the above example, the attribute breed of fish is another struct species.

There are various ways to write the assignment.

// Writing method one
struct fish shark = {"shark", 9, {"Selachimorpha", 500}};

// Write II
struct species myBreed = {"Selachimorpha", 500};
struct fish shark = {"shark", 9, myBreed};

// Write three
struct fish shark = {
.name="shark",
.age=9,
.breed={"Selachimorpha", 500}
};

// Writing method 4
struct fish shark = {
.name="shark",
.age=9,
.breed.name="Selachimorpha",
.breed.kinds=500
};

printf("Shark's species is %s", shark.breed.name);

The above example shows four ways of writing assignments to nested Struct structures. In addition, internal properties that reference the breed attribute use the dot operator (shark.breed.name) twice.

Here is another example of a nested struct.

struct name {
char first[50];
char last[50];
};

struct student {
struct name name;
short age;
char sex;
} student1;

strcpy(student1.name.first, "Harry");
strcpy(student1.name.last, "Potter");

// or
struct name myname = {"Harry", "Potter"};
student1.name = myname;

In the above example, the name attribute of the custom type student is another custom type, and to refer to the latter attribute, you must use the two . operators, such as student1.name.first. Also, to assign a value to a character array property, use the strcpy() function, not directly, as changing the address of the character array name directly will result in an error.

Not only can structs refer to other structs, they can also be self-referential, i.e. they refer to the current struct internally. For example, the nodes of a linked table structure could be written as follows.

struct node {
int data;
struct node* next;
};

In the above example, the next attribute of the node structure is a pointer to another instance of node. Below, use this structure to customize a data link table.

struct node {
int data;
struct node* next;
};

struct node* head;

// Generate a list of three nodes (11)->(22)->(33)
head = malloc(sizeof(struct node));

head->data = 11;
head->next = malloc(sizeof(struct node));

head->next->data = 22;
head->next->next = malloc(sizeof(struct node));

head->next->next->data = 33;
head->next->next->next = NULL;

// iterate through this list
for (struct node *cur = head; cur ! = NULL; cur = cur->next) {
printf("%d\n", cur->data);
}

The above example is the simplest implementation of a linked table structure, which can be traversed by a for loop.

Bit fields

struct can also be used to define data structures consisting of binary bits, called bit fields, which are useful for manipulating the underlying binary data.

struct {
unsigned int ab:1;
unsigned int cd:1;
unsigned int ef:1;
unsigned int gh:1;
} synth;

synth.ab = 0;
synth.cd = 1;

In the above example, the :1 after each property means that it is specified that these properties occupy only one binary bit, so this data structure is a total of 4 binary bits.

Note that when defining the binary bits, each attribute inside the structure can only be of integer type.

When actually stored, C stores a bit field structure according to the number of bytes occupied by the int type. If there are binary bits left over, you can use the unnamed attribute to fill in those bits. It is also possible to use an attribute with a width of 0, indicating that the remaining binary bits that occupy the current byte are filled, forcing the next attribute to be stored in the next byte.

struct {
unsigned int field1 : 1;

unsigned int field2 : 1;

unsigned int field3 : 1;
} stuff;

In the above example, there is an unnamed attribute between stuff.field1 and stuff.field2 that is two binary bits wide. stuff.field3 will be stored in the next byte.

Flexible array membership

Very often it is not possible to determine in advance exactly how many members an array will have. C provides a solution to this problem, called flexible array member.

If the number of array members cannot be determined in advance, a struct structure can be defined.

struct vstring {
int len;
char chars[];
};

The struct vstring structure in the above example has two attributes. The len attribute is used to record the length of the array chars, and the chars attribute is an array, but the number of members is not given.

Exactly how many members there are in the chars array can be determined when allocating memory for vstring.

struct vstring* str = malloc(sizeof(struct vstring) + n * sizeof(char));
str->len = n;

In the above example, it is assumed that the number of members of the chars array is n, and only at runtime will we know exactly what n is. Then, the memory it needs is allocated for struct vstring: the length of memory it occupies by itself, plus the length of memory occupied by the n array members. Finally, the len attribute records how much n is.

This allows the array chars to have n members without having to be determined in advance, and can be kept in line with runtime needs.

There are some special rules for flexible array membership. First, the array of elastic members must be the last attribute of the struct structure. Also, the struct structure must have at least one other attribute in addition to the flexible array member.